'''TODO LIST:
- cascades
- items, item types
- monsters, monster types
- size should be one number
- requests should not be in db at all
'''

from sqlalchemy import Column as col, Integer, String, CHAR, Float,\
                        ForeignKey, create_engine

from sqlalchemy.orm import relationship, sessionmaker, backref
from sqlalchemy.orm.collections import attribute_mapped_collection
from sqlalchemy.ext.declarative import declarative_base, DeclarativeMeta
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.schema import UniqueConstraint
from sqlalchemy.ext.hybrid import hybrid_property
from collections import defaultdict
from settings import *
from twisted.internet.defer import DeferredLock, Deferred, maybeDeferred

engine = create_engine(DATABASE_CONNECTION, echo=False)
'''
from sqlalchemy.pool import StaticPool
engine = create_engine('sqlite:///:memory:',
                    connect_args={'check_same_thread':False},
                    poolclass=StaticPool,
                    echo=False)
'''
Session = sessionmaker(bind=engine)

class NamedLock(DeferredLock):
    
    def acquire(self, name='noname'):
        d = Deferred(canceller=self._cancelAcquire)
        d.name = name
        if self.locked:
            self.waiting.append(d)
        else:
            self.locked = True
            d.callback(self)
        return d
    
    def run(*args, **kwargs): #@NoSelf
        """ Overriden method from twisted to use name """
        if len(args) < 3:
            if not args:
                raise TypeError("run() takes at least 3 arguments")
        self, f, name = args[:3]
        args = args[3:]

        def execute(ignoredResult):
            d = maybeDeferred(f, *args, **kwargs)
            d.addBoth(self._releaseAndReturn)
            return d

        d = self.acquire(name)
        d.addCallback(execute)
        d.addErrback(lambda i:None)
        return d

class Handler(object):
    ''' Never forget to commit or rollback '''

    def __init__(self, commit=False):
        self.session = Session()
        self.add = self.session.add
        self.merge = self.session.merge
        self.do_commit = commit

    def commit(self):
        self.session.commit()
        self.session.close()

    def rollback(self):
        self.session.rollback()
        self.session.close()
        
    def refresh(self, *a, **kw):
        return self.session.refresh(*a, **kw)


    def find_cell(self, x, y, loc_id=None):
        ''' find cell by coords '''
        loc_id = loc_id or 1

        cell = self.session.query(Cell).filter(Cell.xpos==x &
                                               Cell.ypos==y &
                                               Cell.loc_id==loc_id).one()
        return cell

    def login(self, login, md5, cr_id = None):
        s = self.session
        q = s.query(User).filter((User.login == login) &
                                 (User.pwd_md5 == md5))
        if q.count():
            creatures = q.one().creatures
            if not cr_id:
                return creatures
            cr = {str(i.id):i for i in creatures}
            return [cr.get(cr_id, None)]

    def find_free_cell(self, loc_id, creature):
        cells = self.get_location(loc_id).cells
        for coords, c in cells.iteritems():
            target_cells = creature.would_coords(coords)
            if not (target_cells & self.get_location(loc_id).occupied_cells):
                if all([cc in cells and cells[cc].passable for cc in target_cells]):
                    return c


    def draw_map(self, loc_id):
        s = self.session
        l = s.query(Location).filter_by(id=loc_id).one()
        maxx, maxy = l.dimensions
        objects = l.objects
        objects = {o.cell.coords:o for o in objects}
        cells = {c.coords:c for c in l.cells}
        map = ''
        for y in range(maxy):
            line = ''
            for x in range(maxx):
                line += objects.get((x,y),cells[(x,y)]).char
            map += line + '\n'
            line = ''
        return map

    def get_location(self, loc_id):
        return self.session.query(Location).filter_by(id=loc_id).one()

    def __enter__(self):
        return self

    def __exit__(self, *args):
        ''' rollback or commit '''
        if args[0]:
            self.rollback()
            return
        self.commit() if self.do_commit else self.rollback()

    def __del__(self):
        self.session.close()
        
        
        
class NonDbData(object):
    
    def __init__(self, dic):
        self.dict = dic
        
    def __get__(self, obj, type):
        return obj.id and self.dict.get(obj.id, None)
    
    def __set__(self, obj, value):
        assert obj.id, "Assigning to object without id"
        self.dict[obj.id] = value
        
        
        

class Coord(tuple):
    def __add__(self, c):
        return Coord([i + j for i, j in zip(self, c)])

    def __str__(self):
        return "{},{}".format(*self)
    
    def dist((x1,y1),(x2,y2)): #@NoSelf
        return ((x2-x1)**2 + (y2-y1)**2)**0.5

class TableNamesFromClassNames(DeclarativeMeta):
    def __init__(cls, *a, **kw): #@NoSelf
        cls.__tablename__ = cls.__name__.lower()+'s'
        super(TableNamesFromClassNames, cls).__init__(*a, **kw)

############### MAPPING ##############

Base = declarative_base(metaclass=TableNamesFromClassNames)

class Object(Base):
    
    id = col(Integer, primary_key=True)
    type = col('type', String(50))
    __mapper_args__ = {'polymorphic_on': type}

    cell_id = col(Integer, ForeignKey('cells.id'))
    cell = relationship("Cell", lazy="joined", backref = backref('objects',lazy='joined'))
    location = association_proxy("cell","location")

    char = col(CHAR, default='?')

    size = col(Integer, default = 1)

    def would_coords(self, pos):
        return {Coord(pos) + (x, y)\
         for x in range(self.size) for y in range(self.size)}

    def fits(self, cell):
        target = self.would_coords(cell.coords) - self.would_coords(self.coords)
        cells = cell.location.cells
        return all([c in cells and cells[c].passable for c in target])

    @property
    def coords(self):
        return self.cell and self.cell.coords

    @property
    def short_info(self):
        return dict(type = self.type, model = self.model, id = self.id)

    @property
    def cells(self):
        cells = set()
        start = self.coords
        if not start:
            return None
        for x in range(self.size):
            for y in range(self.size):
                cells.add(start + (x, y))
        return cells

    def __str__(self):
        return self.char

    def __repr__(self):
        return "<Object({}:{!r}:{})".format(self.id, self.char, self.type)

class Creature(Object):
    
    def __init__(self, template):
        assert isinstance(template, CrTemplate)
        self.template = template

    __mapper_args__ = {'polymorphic_identity': 'creature'}
        
    id = col(Integer, ForeignKey("objects.id"), primary_key=True)
    user_login = col(String, ForeignKey("users.login"))
    sight = col(Integer, default = 6) # radius of sight
    visible_cells=association_proxy('visibilities', 'cell')
    max_life = col(Integer, default = 20)
    
    template_id = col(Integer, ForeignKey("crtemplates.id"))
    template = relationship("CrTemplate")
    
    model = association_proxy('template','model')
    name = association_proxy('template','name')
    descr = association_proxy('template','descr')

    def __str__(self):
        return "({}){}:{}".format(self.id, self.user_login, self.name)

    __repr__ = __str__


class CrTemplate(Base):
    # TODO: divide into npcCreature and playerCreature
    id = col(Integer, primary_key=True)
    str = col(Integer)
    dex = col(Integer)
    int = col(Integer)
    # creatureattrs =  relationship("CrTemplAttribute") TODO
    life = col(Integer)
    mana = col(Integer)
    
    name = col(String)
    descr = col(String)
    model = col(String)


class ItemTemplate(Base):
    id = col(Integer, primary_key=True)
    name = col(String)
    descr = col(String)
    model = col(String)
    weight = col(Float)
    type = col(String)
    max_dur = col(Integer)
    __mapper_args__ = {'polymorphic_on': type}
    #requirements will be in attributes
    #itemattrs =  relationship("ItemTemplAttribute") TODO


class WeaponTemplate(ItemTemplate):
    __mapper_args__ = {'polymorphic_identity': 'weapon'}
    id = col(Integer, ForeignKey(ItemTemplate.id), primary_key=True)
    min_damage = col(Integer)
    max_damage = col(Integer)
    
    
class User(Base):

    login = col(String, primary_key=True)
    pwd_md5 = col(String)
    creatures = relationship(Creature, backref="user")
    admin = col(CHAR)

    def __str__(self):
        return self.login

    def __repr__(self):
        return '<User({})>'.format(self.login)


class Cell(Base):
    __table_args__ = (
        UniqueConstraint('xpos', 'ypos', 'loc_id', name='cells_uc'),)

    id = col(Integer, primary_key=True)
    xpos = col(Integer)
    ypos = col(Integer)
    loc_id = col(Integer, ForeignKey("locations.id"))
    char = col(CHAR)

    @hybrid_property
    def floor(self):
        return self.char == '.'

    @property
    def passable(self):
        return self.floor and self.coords not in self.location.occupied_cells

    @property
    def coords(self):
        return Coord((self.xpos, self.ypos))

    @coords.setter
    def coords(self, c):
        self.xpos, self.ypos = c

    def __str__(self):
        return self.char

    def __repr__(self):
        return "<Cell {} ({},{}):{}>".format(self.char, self.xpos, self.ypos, self.loc_id)


class Visibility(Base):

    def __init__(self, cell):
        self.cell = cell
        self.new = 'Y'
    id = col(Integer, primary_key=True)
    creature_id = col(Integer, ForeignKey('creatures.id'))
    creature = relationship(Creature, backref="visibilities")
    cell_id = col(Integer, ForeignKey('cells.id'))
    cell = relationship(Cell, backref="observed_by")
    new = col(CHAR)  # if the visibility changed and user was not notified


class Location(Base):
    
    _locks = defaultdict(NamedLock)
    lock = NonDbData(_locks)
    
    _updaters = {}
    updater = NonDbData(_updaters)
    
    _requests = defaultdict(list)
    requests = NonDbData(_requests)

    id = col(Integer, primary_key=True)
    name = col(String)
    cells = relationship(Cell, collection_class=attribute_mapped_collection("coords"), backref="location")
    objects = relationship(Object, secondary = "cells",
                           collection_class = attribute_mapped_collection("coords"),
                           primaryjoin="Location.id == Cell.loc_id",
                           secondaryjoin = "Cell.id == Object.cell_id")
    
    items = relationship(Object, secondary = "cells",
                           collection_class = attribute_mapped_collection("coords"),
                           primaryjoin="Location.id == Cell.loc_id",
                           secondaryjoin = "(Cell.id == Object.cell_id) &"
                                           "(Object.type == 'weapon')")

    creatures = relationship(Object, secondary = "cells",
                             collection_class = attribute_mapped_collection("coords"),
                             primaryjoin="Location.id == Cell.loc_id",
                             secondaryjoin = "(Cell.id == Object.cell_id) &"
                                             "(Object.type == 'creature')")
    
    @property
    def id_objects(self):
        return {o.id:o for o in self.objects.values()}
    
    @property
    def id_items(self):
        return {o.id:o for o in self.items.values()}    
    
    def draw(self):
        data = ''
        chars = {}
        chars.update({c:o.char for c,o in self.cells.items()})
        chars.update({c:o.char for c,o in self.items.items()})
        chars.update({c:o.char for c,o in self.creatures.items()})
        for y in range(self.dimensions[1]):
            line = ''
            for x in range(self.dimensions[0]):
                line += chars.get((x,y),' ')
            data += line + '\n'
            
        return data 

    @property
    def item_cells(self):
        return {o.id:o.cells for o in self.items.itervalues()}

    @property
    def occupied_cells(self):
        cells = set()
        for c in self.creatures.itervalues():
            cells.update(c.cells)
        return cells

    @property
    def dimensions(self):
        x = max([c[0] for c in self.cells])
        y = max([c[1] for c in self.cells])
        return (x+1,y+1)

    def __str__(self):
        return self.name

    def __repr__(self):
        return '<Location:{}({})>'.format(self.name,self.id)


class Item(Object):
    '''Something, that any player can pick (if he has enough strength)'''
    id = col(Integer, ForeignKey('objects.id'), primary_key = True)
    weight = col(Float)
    dur = col(Integer)
    max_dur = col(Integer)
    model = association_proxy('template','model')
    name = association_proxy('template','name')
    descr = association_proxy('template','descr')
    creature_id = col(Integer, ForeignKey(Creature.id))
    creature = relationship(Creature, backref='inventory', 
                            primaryjoin="Item.creature_id==Creature.id")


class Weapon(Item):
    __mapper_args__ = {'polymorphic_identity': 'weapon'}
    id = col(Integer, ForeignKey('items.id'), primary_key = True)
    template_id = col(Integer, ForeignKey(WeaponTemplate.id))
    template = relationship(WeaponTemplate)
    min_damage = col(Integer, nullable=False)
    max_damage = col(Integer, nullable=False)
    
    def __init__(self, template):
        assert isinstance(template, WeaponTemplate), "wrong argument to init"
        super(Weapon, self).__init__()
        self.template = template
        self.min_damage = template.min_damage
        self.max_damage = template.max_damage
        self.dur = self.max_dur = template.max_dur
    
class MagicAttr(Base):
    id = col(Integer, primary_key=True)
    object_id = col(Integer, ForeignKey('objects.id'), nullable=False)
    name = col(String)
    value = col(String)

def create():
    session = Session()
    Base.metadata.create_all(engine)
    import create_data
    create_data.create(session)

# create tables here:
if __name__ == '__main__':
    create()
